﻿// --------------------------------------------------------------------------------------------------------------------
// <copyright file="InMemoryCacheProvider.cs" company="XamlNinja">
//   2011 Richard Griffin and Ollie Riches
// </copyright>
// <summary>
//   In memory cache provider - uses a dictionary class to store the data, data is not persisted when application is tombstoned or closed.
// </summary>
// --------------------------------------------------------------------------------------------------------------------

namespace WP7Contrib.Caching
{
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Reactive.Linq;
    using Logging;

    /// <summary>
    /// In memory cache provider - uses a dictionary class to store the data, data is not persisted when application is tombstoned or closed.
    /// </summary>
    public sealed class InMemoryCacheProvider : ICacheProvider, IDisposable
    {
        /// <summary>
        /// The dictionary containing the key value pairs - the key must implement a hashing algorithm that generates unique hash values.
        /// </summary>
        private readonly IDictionary<object, object> dictionary = new Dictionary<object, object>();

        /// <summary>
        /// The logger used to capture diagnostics information.
        /// </summary>
        private readonly ILog log;

        /// <summary>
        /// The sync object to make accessing the dictionary is thread safe.
        /// </summary>
        private readonly object sync = new object();

        /// <summary>
        /// Initializes a new instance of the <see cref="InMemoryCacheProvider"/> class.
        /// </summary>
        public InMemoryCacheProvider()
        {
            this.log = new DebugLog();
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="InMemoryCacheProvider"/> class.
        /// </summary>
        /// <param name="log">
        /// The logger used to capture diagnostics information.
        /// </param>
        public InMemoryCacheProvider(ILog log)
        {
            this.log = log;
        }

        public void Dispose()
        {
            this.dictionary.Clear();
        }

        #region ICacheProvider Members

        /// <summary>
        /// Add a value to the cache with a relative expiry time, e.g 10 minutes.
        /// </summary>
        /// <typeparam name="TKey">
        /// The type of the key.
        /// </typeparam>
        /// <typeparam name="TValue">
        /// The type of the value.
        /// </typeparam>
        /// <param name="key">
        /// The key value.
        /// </param>
        /// <param name="value">
        /// The value, can be null.
        /// </param>
        /// <param name="relativeTime">
        /// The relative date time when the key value pair should expire and be purged from the cache.
        /// </param>
        public void Add<TKey, TValue>(TKey key, TValue value, TimeSpan relativeTime) where TValue : class
        {
            this.AddImpl(key, value, relativeTime);
        }

        /// <summary>
        /// Add a value to the cache with an absolute time, e.g. 01/01/2020.
        /// </summary>
        /// <typeparam name="TKey">
        /// The type of the key.
        /// </typeparam>
        /// <typeparam name="TValue">
        /// The type of the value.
        /// </typeparam>
        /// <param name="key">
        /// The key value.
        /// </param>
        /// <param name="value">
        /// The value, can be null.
        /// </param>
        /// <param name="absoluteTime">
        /// The absolute date time when the cache should expire and be purged the value.
        /// </param>
        public void Add<TKey, TValue>(TKey key, TValue value, DateTime absoluteTime) where TValue : class
        {
            if (absoluteTime < DateTime.Now)
            {
                return;
            }

            var diff = absoluteTime - DateTime.Now;
            this.AddImpl(key, value, diff);
        }

        /// <summary>
        /// Gets a value from the cache.
        /// </summary>
        /// <typeparam name="TKey">
        /// The type of the key.
        /// </typeparam>
        /// <typeparam name="TValue">
        /// The type of the value.
        /// </typeparam>
        /// <param name="key">
        /// The key associated with value.
        /// </param>
        /// <returns>
        /// If the key exists in the cache then the value is returned, if the key does not exist then null is returned.
        /// </returns>
        public TValue Get<TKey, TValue>(TKey key) where TValue : class
        {
            object value;
            if (this.dictionary.TryGetValue(key, out value))
            {
                return (TValue)value;
            }

            return null;
        }

        /// <summary>
        /// Remove a value from the cache.
        /// </summary>
        /// <param name="key">
        /// The key associated with the value.
        /// </param>
        /// <typeparam name="TKey">
        /// The type of the key.
        /// </typeparam>
        public void Remove<TKey>(TKey key)
        {
            if (Equals(key, null))
            {
                return;
            }

            this.Purge(key);
        }

        /// <summary>
        /// Clears the contents of the cache.
        /// </summary>
        public void Clear()
        {
            lock (this.sync)
            {
                this.dictionary.Clear();
            }
        }

        /// <summary>
        /// Nothing to preemptively do for this caching implementation
        /// </summary>
        public void PreemptiveInitialise()
        {
        }

        /// <summary>
        /// All values can be cached with this implementation.
        /// </summary>
        /// <typeparam name="T">The type of the value.</typeparam>
        /// <param name="value">The value to checkd for cachability.</param>
        /// <param name="failingTypes">The types failing </param>
        /// <returns>Returns true if the value can be added to the cache.</returns>
        public bool IsCacheable<T>(T value, ref IEnumerable<Type> failingTypes) where T : class
        {
            return true;
        }

        /// <summary>
        /// Gets an enumerator for keys of a specific type.
        /// </summary>
        /// <typeparam name="TKey">The type of the key.</typeparam>
        /// <returns>Returns an enumerator for keys of a specific type.</returns>
        public IEnumerable<TKey> Keys<TKey>()
        {
            lock (sync)
            {
                return this.dictionary.Keys.Where(k => k.GetType() == typeof (TKey)).Cast<TKey>().ToList();
            }
        }

        #endregion

        /// <summary>
        /// The implementation of adding a value to the cache with an absolute time, e.g. 01/01/2020.
        /// </summary>
        /// <typeparam name="TKey">
        /// The type of the key.
        /// </typeparam>
        /// <typeparam name="TValue">
        /// The type of the value.
        /// </typeparam>
        /// <param name="key">
        /// The key value.
        /// </param>
        /// <param name="value">
        /// The value, can be null.
        /// </param>
        /// <param name="relative">
        /// The relative date time when the cache should expiry and be purged the value.
        /// </param>
        private void AddImpl<TKey, TValue>(TKey key, TValue value, TimeSpan relative)
        {
            lock (this.sync)
            {
                Observable.Timer(relative)
                    .Finally(() =>
                    {
                        GC.Collect();
                        GC.WaitForPendingFinalizers();
                    })
                    .Subscribe(x => this.Purge(key), 
                    exn => this.log.Write("InMemoryCacheProvider: Purge Failed - '{0}'", exn.Message), 
                    () => this.log.Write("InMemoryCacheProvider: Purge Completed..."));

                this.dictionary[key] = value;
            }

            this.log.Write("InMemoryCacheProvider: Added Value to cache, time: {0}, hash code {1}", DateTime.Now, key.GetHashCode());
        }

        /// <summary>
        /// Purges an entry from the dictionary, this is thread safe.
        /// </summary>
        /// <param name="key">
        /// The key of the key value pair to be removed.
        /// </param>
        private void Purge(object key)
        {
            lock (this.sync)
            {
                this.dictionary.Remove(key);
            }

            this.log.Write("InMemoryCacheProvider: Purged value from cache, time: {0}, hash code {1}", DateTime.Now, key.GetHashCode());
        }
    }
}